Skip to content

Move the scraper off Playwright to httpx + containerize for the k8s CronJob - #44

Merged
aatchison merged 20 commits into
mainfrom
k8s-argocd-scraper-deployment
Jul 27, 2026
Merged

Move the scraper off Playwright to httpx + containerize for the k8s CronJob#44
aatchison merged 20 commits into
mainfrom
k8s-argocd-scraper-deployment

Conversation

@aatchison

@aatchison aatchison commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Phase A of moving the AAQ scraper onto a Kubernetes CronJob. Pairs with platform-infrastructure#802 (manifests, ArgoCD app, Pulumi). Relates to #27, #28.

This PR is additive — it removes no working capability. It does not touch extension/, import_json.py, the CSV writers, or any workflow other than adding a new one.

Why

support.mozilla.org is behind a Fastly bot-challenge that fingerprints automated browsers and non-allowlisted source IPs, so the Playwright approach is dead (#28) and Actions runners can't reach the API.

The workloads EKS cluster's NAT egress IPs are already allowlisted — verified 2026-07-27 from a pod in each AZ using this branch's exact httpx client: 3.67.52.124 and 63.182.70.185 both return HTTP 200 + JSON, while the identical request from a non-allowlisted network returns the challenge HTML.

⚠️ A local run still raises ChallengeError. That is expected (your workstation isn't allowlisted) and is not an outage.

What's here

  • sumo.py: Playwright → httpx. The SumoBrowser public API is frozen — class name, all __init__ kwargs, and the fetch_json/paginate method bodies are byte-identical. Only __init__/__enter__/__exit__/_raw_fetch changed, so no consumer needed edits, and the retry/deferral/challenge contract (ChallengeError, RateLimitDeferral, exit 75) is preserved.
  • Playwright kept as an optional dependency groupuv sync --group playwright still runs poc.py. Not installed by default; verified absent from the image.
  • Dockerfile — browser-free python:3.12-slim, arm64, nonroot.
  • deploy/entrypoint.sh — clone → run_refresh.py → commit CSVs + .refresh-hwm → push with rebase-retry. PAT supplied via a git credential helper, never in argv or a URL.
  • .refresh-hwm tracked in git (the pod is stateless; the repo is the state).
  • aaq-scraper-image.yml — arm64 build → shared ECR via OIDC.
  • Design + plan under docs/superpowers/.

Relationship to the browser extension

main moved on while this was in flight, and the extension is what currently produces data (imported through 2026-07-26); all three GitHub workflows are disabled_manually. This branch is merged up to date with that work and leaves it untouched.

Usefully, import_json.py reuses the scrapers' own writers (build_fieldnames, flatten_answer, COLUMNS), so extension-imported and run_refresh.py-generated CSVs are format-compatible — no churn if both run.

Open decision, deliberately not made here: whether the CronJob replaces the extension or runs beside it. Both would commit the same day-CSVs on main. The CronJob ships suspended, so this is decided at cutover, not at merge.

Testing

11/11 passing, plus container-level verification:

  • tests/test_fetch_json.py — characterization tests stubbing only the transport, so they pin the retry/deferral/challenge semantics and passed against the old Playwright code before the swap and after it. Plus MockTransport tests for the httpx→dict mapping.
  • tests/test_entrypoint.py — integration against a local bare git repo (no network/token): new CSV → one commit; unchanged run → none; and a competing push forces the rebase-retry path, asserting both commits survive.
  • Real image under docker run --read-only: full clone → refresh → commit works, and uv run reuses the baked venv (sys.prefix=/app/.venv, no PyPI fetch) via the four UV_* vars now in the CronJob spec.

Review notes

Issues found and fixed during review:

  • PAT was initially in the clone URL (visible via ps) → replaced with a credential helper.
  • Image workflow interpolated ${{ github.event.inputs.tag }} into a run: block — script injection in a job holding ECR-push creds → passed via env + charset validation.
  • arm64 build had no QEMU step; would have failed on merge.
  • An earlier commit parking schema-check.yml was reverted — the workflow is already disabled at the GitHub level, so it changed nothing and its rationale was false.

Merge #802 first — it creates the OIDC role this repo's image workflow assumes.

🤖 Generated with Claude Code

Egress-IP-first scaffolding to move the scraper off GitHub Actions
(blocked by the Fastly bot-challenge, #28) onto the workloads EKS
cluster, sharing bitergia's static NAT egress IP for the Mozilla
allowlist ask (#27). Plain-HTTP image, CSVs stay in git, fine-grained
PAT for pushes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@aatchison
aatchison marked this pull request as draft July 13, 2026 17:40
aatchison and others added 16 commits July 13, 2026 10:46
Two-phase, TDD where testable: Phase A (this repo) swaps sumo.py's
Playwright engine for httpx behind the frozen SumoBrowser API, packages
a browser-free arm64 image whose entrypoint clones/refreshes/commits,
and builds it in CI; Phase B (platform-infrastructure) adds the Pulumi
ECR+OIDC, the deploy manifests, and the ArgoCD app.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The sandbox default is Python 3.14, against which playwright's greenlet
dependency has no wheel and fails to build, breaking `uv sync`/`uv run`.
Pin to 3.12 (within requires-python >=3.10) so the mandated `uv run
pytest` works. (greenlet/playwright are removed in the next task, but the
pin keeps the toolchain reproducible regardless of the host default.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CronJob commits the high-water mark to the repo each active run
instead of the (unavailable) GitHub Actions cache. run_refresh.py's 26h
lookback covers its initial absence, so no seed file is needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pass the workflow_dispatch tag input via an env var instead of
interpolating github.event.* into the run script, and validate its
charset. Flagged HIGH (ci-cd-script-injection) by review; the job holds
ECR-push OIDC creds. Behavior unchanged for the default git-<sha> path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oint verification

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rtanglao

Copy link
Copy Markdown
Collaborator

looks like a great spec to me @aatchison , love to have this working before end of july please and thank-you :-), I know e are all busy :-) so if not we have my Chrome and Firefox extension as a fallback cc / @lisajill

The workloads cluster's NAT egress IPs are already allowlisted by
Mozilla; the earlier "still blocked" conclusion came from testing on a
workstation rather than in the cluster. Verified 2026-07-27 from a pod in
each AZ with the scraper's own httpx client: 3.67.52.124 and
63.182.70.185 both return 200 + JSON, while the same request from a
non-allowlisted network returns the challenge HTML.

So the CronJob works as soon as it is deployed -- the remaining gate is
mechanical (image build, PAT secret, unsuspend), not an upstream ask.

Also records that the read-only-root-FS question is resolved: the real
image runs the full clone/refresh/commit flow with uv reusing the baked
venv, given the four UV_* env vars now set in the CronJob.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@aatchison aatchison changed the title Design: run AAQ scraper as an ArgoCD CronJob on the workloads cluster Move the scraper off Playwright to httpx + containerize for the k8s CronJob Jul 27, 2026
@aatchison
aatchison marked this pull request as ready for review July 27, 2026 17:55
aatchison and others added 2 commits July 27, 2026 11:07
…ema-check edit

Brings the branch up to date with main (66 commits, incl. the browser
extension and its imported data), and makes this PR strictly non-destructive:

- pyproject.toml: playwright moves to an OPTIONAL dependency group rather
  than being removed outright, so poc.py and any manual browser experiment
  still work via `uv sync --group playwright`. It is not installed by
  default and is absent from the container image (verified).
- .github/workflows/schema-check.yml: reverted to main. The earlier "park
  the daily cron" commit was based on a false premise -- all three
  workflows are already disabled_manually at the GitHub level, so the edit
  changed nothing and its rationale (stop spurious api-blocked issues) was
  wrong; a disabled workflow opens none.
- .gitignore: keep main's extension rules AND the .refresh-hwm un-ignore,
  with a note explaining why the latter is deliberate.
- Docs (CLAUDE.md, README, spec, plan, poc.py): drop the claim that
  scrape.yml is the live production refresh -- it is disabled and last ran
  2026-07-10. Record that the extension is what currently produces data,
  that its output is format-compatible with run_refresh.py (import_json.py
  reuses the scrapers' writers), and flag the open decision: whether the
  CronJob replaces the extension or runs beside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The k8s CronJob needs to run alongside the browser extension without both
committing the same day-CSVs. Three small, default-off knobs:

- AAQ_DATA_ROOT prefixes the <year>/ tree in default_output_path(). Unset
  (the default) keeps the exact committed layout, so nothing changes for
  existing runs.
- run_refresh.py now derives the questions path from that same helper
  instead of formatting it a second time, so the path can only be built
  one way.
- entrypoint.sh takes GIT_ADD_PATHS (default `20*/ .refresh-hwm`) and
  stages each path independently, tolerating ones that don't exist. A
  deployment writing under a data root points this at that directory, so
  the committed CSVs cannot be staged even by accident.

Also fixes a real fragility the new isolation test exposed: `git pull
--rebase` refuses to run with a dirty working tree, so any unstaged file
left in the clone would wedge the push-retry loop for all five attempts
and fail an otherwise-good run. Now uses --autostash.

Tests: default layout unchanged; AAQ_DATA_ROOT relocates; and an
end-to-end entrypoint test asserting that with GIT_ADD_PATHS set only the
shakedown directory is committed while a concurrently-modified real CSV
is left alone. 14/14 passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants